package com.liu.yupao.ws;

import cn.hutool.json.JSONUtil;
import com.liu.yupao.config.GetHttpSessionConfig;
import com.liu.yupao.model.entity.User;
import com.liu.yupao.model.ws.Message;
import com.liu.yupao.utils.MessageUtils;
import org.springframework.context.annotation.Configuration;

import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author lqh
 * @since 2025-11-10 22:00
 * configurator = GetHttpSessionConfig.class 获取HttpSession对象
 */
@ServerEndpoint (value = "/chat",configurator = GetHttpSessionConfig.class)
@Configuration
public class ChatEndPoint {

    /**
     * 存储在线用户数据
     * final：防止给变量重新赋值
     * static：如果不使用static关键字，这个集合就会只属于一个对象（类）
     * 但是EndPoint对象是针对每一个客户端连接都会创建一个对象，这样就都会创建一个onlineUsers变量
     * 这个ChatEndPoint是多例的，我们想所有的endPoint都使用同一个集合，所有要使用static关键字（共用）

     * 线程安全问题：
     * 线程安全：多线程并发操作共享资源时，结果始终正确（需通过同步机制保证）。
     * 线程不安全：多线程并发操作共享资源时，可能因操作交叉导致数据错乱（默认无同步机制时常见）。
     *
     * Map<String,Session>  string：用户名 session：浏览器信息
     */
    private static final Map<String,Session> onlineUsers=new ConcurrentHashMap<> ();  //线程安全的集合

    //因为下面几个方面都需要使用到httpSession 在onOpen方法中 把获取到的httpSession赋值给该变量 下面在使用就不需要重复获取了
    private HttpSession httpSession;



    /**
     * 建立websocket连接时被调用
     * @param session  websocket的session
     * @param config 就是GetHttpSessionConfig配置中ServerEndpointConfig对象（当时也是存储到这里来的）
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        //1.将session进行保存（保存浏览器（客户端）的信息 因为是一一对立的）
        //获取登录用户信息
        this.httpSession = (HttpSession)config.getUserProperties ().get (HttpSession.class.getName ());
        User user = (User) this.httpSession.getAttribute ("USER_LOGIN_STATE");
        onlineUsers.put (user.getUserAccount (),session);
        //2.广播消息（将登录的所有的用户推送给所有的用户）  系统推送消息格式{"system":true,"fromName":null,"message":["李四","王五"]}
        String message = MessageUtils.getMessage (true, null, getFriends ());
        broadcastAllUsers (message);

    }

    /**
     * 浏览器发送消息到服务端，该方法会被调用
     * @param message 浏览器（客户端）发送过来的消息
     * 张三--->李四  不是直接点对点 而是先把消息发送给服务器 服务器在推送给指定客户端
     * 客户端--->服务器
     * 格式：{"toName":"张三","message":"你好"}
     */
    @OnMessage
    public void onMessage(String message) {
        try {
            //将消息发送给执行客户端
            Message msg1 = JSONUtil.toBean (message, Message.class);
            //获取接收消息的用户名
            String toName = msg1.getToName ();
            //获取发送信息数据
            String mess = msg1.getMessage ();
            //发送给李四
            Session session = onlineUsers.get (toName);
            User user = (User) this.httpSession.getAttribute ("USER_LOGIN_STATE");
            //推送给某一个用户的消息格式 {"system":false,"fromName":"张三","message":"你好"}
            MessageUtils.getMessage (false, user.getUserAccount (), mess);
            session.getBasicRemote ().sendText (message);
        }catch (Exception e){
            //记录日志
        }

    }

    /**
     * 断开websocket连接时被调用
     * @param session  websocket的session
     */
    @OnClose
    public void onClose(Session session) {
        //从onlineUsers剔除当前退出的用户
        User user = (User) this.httpSession.getAttribute ("USER_LOGIN_STATE");
        onlineUsers.remove (user.getUserAccount ());
        //通知其他所有用户，当前用户下线了
        broadcastAllUsers (MessageUtils.getMessage (true, null, getFriends ()));
    }

    //广播消息 将登录成员信息广播给其他登录用户
    /**
     * @param message 广播消息（系统消息）
     * 格式：{"system":true,"fromName":null,"message":["李四","王五"]}
     */
    public void broadcastAllUsers(String message) {
        try {
            //遍历map
            Set<Map.Entry<String, Session>> entrySet = onlineUsers.entrySet ();
            for (Map.Entry<String, Session> entry : entrySet) {
                //获取当前登录用户对应的session对象
                Session session = entry.getValue ();
                //发送消息  同步发送
                session.getBasicRemote ().sendText (message);
            }
        }catch (Exception e){
            //记录日志
        }
    }

    /**
     * 获取登录用户数据
     * 这里的key就是用户名 所以这里使用set类型比较好
     */
    public Set getFriends(){
        return onlineUsers.keySet ();
    }
}
